using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using JWT;
using JWT.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace UploadServer.middlewares
{
    /// <summary>
    /// 分块上传
    /// 客户端：https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md
    /// </summary>
    public class ChunkUploadMiddleware : IMiddleware
    {
        private UploadServerConfig _config;

        /// <summary>
        /// 分片临时根目录
        /// </summary>
        private string temporaryFolder = "temp";

        /// <summary>
        /// 记录上传文件块的数量，用于合并前的检测
        /// </summary>
        private ConcurrentDictionary<string, int> countDict =
            new ConcurrentDictionary<string, int>();

        public ChunkUploadMiddleware(IOptions<UploadServerConfig> config)
        {
            _config = config.Value;
        }

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
            {
                context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
                context.Response.Headers.Add("Access-Control-Allow-Headers", "content-type,jwt,origin");
                context.Response.StatusCode = (int)HttpStatusCode.OK;
            }
            else if (context.Request.Method.Equals(HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
            {
                context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
                //简单实现
                context.Request.Query.TryGetValue("chunkNumber", out var chunkNumbers);
                int.TryParse(chunkNumbers.ToString(), out var chunkNumber);
                context.Request.Query.TryGetValue("identifier", out var identifiers);
                if (chunkNumber == 0 || string.IsNullOrEmpty(identifiers))
                {
                    context.Response.StatusCode = 204;
                }
                else
                {
                    var chunkFilename = getChunkFilename(_config.PhysicalPath, chunkNumber, identifiers);
                    if (File.Exists(chunkFilename))
                    {
                        await context.Response.WriteAsync("found");
                    }
                    else
                    {
                        context.Response.StatusCode = 204;
                    }
                }
            }
            else if (context.Request.Method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
            {
                //验证jwt
                string token = null;
                if (context.Request.Headers.TryGetValue("jwt", out StringValues jwt))
                {
                    token = jwt.ToString();
                }
                else if (context.Request.Form.TryGetValue("jwt", out jwt))
                {
                    token = jwt.ToString();
                }
                else
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "No JWT in the header and form"
                    }.toJson());
                    return;
                }

                try
                {
                    var payload = new JwtBuilder().WithSecret(_config.JWTSecret).MustVerifySignature()
                        .Decode<JwtPayload>(token);
                    var msg = payload.validate();
                    if (msg != null)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = msg
                        }.toJson());
                        return;
                    }

                    //特定的配置
                    var appConfig = _config.GetAppConfig(payload.app);

                    //跨域
                    context.Request.Headers.TryGetValue("Origin", out var origins);
                    var origin = origins.ToString();
                    if (!string.IsNullOrEmpty(origin) && appConfig.IsAllowOrigin(origin))
                    {
                        context.Response.Headers.Add("Access-Control-Allow-Origin", origin);
                    }

                    //获取上传的文件分片
                    var file = context.Request.Form.Files.FirstOrDefault();
                    if (file == null || file.Length == 0)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "There is no file data"
                        }.toJson());
                        return;
                    }

                    //后缀验证
                    var ext = Path.GetExtension(file.FileName);
                    if (!(payload.exts + _config.AllowExts).Contains(ext, StringComparison.OrdinalIgnoreCase)
                        || appConfig.LimitExts.Contains(ext, StringComparison.OrdinalIgnoreCase))
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "File extension is not allowed"
                        }.toJson());
                        return;
                    }
                    
                    //获取参数                    
                    getParams(context, out var chunkNumber, out var chunkSize, out var totalSize, out string identifier,
                        out string filename, out int totalChunks);

                    //验证参数
                    var validMsg = validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename, file.Length, totalChunks, payload.GetByteSize() ?? _config.GetByteSize());
                    if (validMsg != null)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = validMsg
                        }.toJson());
                        return;
                    }
                    else
                    {
                        var chunkFilename = getChunkFilename(_config.PhysicalPath, chunkNumber, identifier);
                        try
                        {
                            using (var fileStream = File.OpenWrite(chunkFilename))
                            {
                                var stream = file.OpenReadStream();
                                stream.CopyTo(fileStream);
                                fileStream.Flush(true);
                                countDict.AddOrUpdate(identifier, 1, (key, oldValue) => oldValue + 1);
                            }

                            if (chunkNumber == totalChunks)
                            {
                                //验证块的完整性
                                while (true)
                                {
                                    if (countDict.GetValueOrDefault(identifier) < totalChunks)
                                    {
                                        await Task.Delay(TimeSpan.FromMilliseconds(500));
                                    }
                                    else
                                    {
                                        countDict.Remove(identifier, out _);
                                        break;
                                    }
                                }

                                //merge file;
                                string[] chunkFiles = Directory.GetFiles(
                                    Path.Combine(_config.PhysicalPath, temporaryFolder),
                                    "uploader-" + identifier + ".*",
                                    SearchOption.TopDirectoryOnly);
                                var fileUrl = await MergeChunkFiles(payload, ext, chunkFiles);
                                await context.Response.WriteAsync(new UploadResult()
                                {
                                    ok = true,
                                    url = fileUrl
                                }.toJson());
                            }
                            else
                            {
                                await context.Response.WriteAsync("partly_done");
                                return;
                            }
                        }
                        catch (Exception exp)
                        {
                            await context.Response.WriteAsync(new UploadResult()
                            {
                                msg = exp.Message
                            }.toJson());
                            return;
                        }
                    }
                }
                catch (TokenExpiredException)
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "Token has expired"
                    }.toJson());
                }
                catch (SignatureVerificationException)
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "Token has invalid signature"
                    }.toJson());
                }
            }
            else
            {
                context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
                await context.Response.WriteAsync($"Request method '{context.Request.Method}' is not supported");
            }
        }

        /// <summary>
        /// 获取参数
        /// </summary>
        /// <param name="context"></param>
        /// <param name="chunkNumber"></param>
        /// <param name="chunkSize"></param>
        /// <param name="totalSize"></param>
        /// <param name="identifier"></param>
        /// <param name="filename"></param>
        /// <param name="totalChunks"></param>
        void getParams(HttpContext context, out int chunkNumber, out int chunkSize, out long totalSize,
            out string identifier, out string filename, out int totalChunks)
        {
            context.Request.Form.TryGetValue("chunkNumber", out var chunkNumbers);
            int.TryParse(chunkNumbers.ToString(), out chunkNumber);

            context.Request.Form.TryGetValue("chunkSize", out var chunkSizes);
            int.TryParse(chunkSizes.ToString(), out chunkSize);

            context.Request.Form.TryGetValue("totalSize", out var totalSizes);
            long.TryParse(totalSizes.ToString(), out totalSize);

            context.Request.Form.TryGetValue("identifier", out var identifiers);
            identifier = identifiers.ToString();
            if (!string.IsNullOrWhiteSpace(identifier))
            {
                identifier = Regex.Replace(identifier, "[^0-9A-Za-z_-]", "", RegexOptions.Multiline);
            }

            context.Request.Form.TryGetValue("filename", out var filenames);
            filename = filenames.ToString();

            context.Request.Form.TryGetValue("totalChunks", out var totalChunkss);
            int.TryParse(totalChunkss.ToString(), out totalChunks);
        }

        /// <summary>
        /// 获取分块文件名
        /// </summary>
        /// <param name="physicalPath"></param>
        /// <param name="chunkNumber"></param>
        /// <param name="identifier"></param>
        /// <returns></returns>
        string getChunkFilename(string physicalPath, int chunkNumber, string identifier)
        {
            // What would the file name be?
            var temp = Path.Combine(physicalPath, temporaryFolder);
            if (!Directory.Exists(temp))
            {
                Directory.CreateDirectory(temp);
            }

            return Path.Combine(physicalPath, temporaryFolder, "uploader-" + identifier + "." + chunkNumber);
        }

        /// <summary>
        /// 验证参数
        /// </summary>
        /// <param name="chunkNumber"></param>
        /// <param name="chunkSize"></param>
        /// <param name="totalSize"></param>
        /// <param name="identifier"></param>
        /// <param name="filename"></param>
        /// <param name="fileSize"></param>
        /// <param name="totalChunks"></param>
        /// <param name="maxFileSize"></param>
        /// <returns></returns>
        private string validateRequest(int chunkNumber, int chunkSize, long totalSize, string identifier, string filename,
            long fileSize, int totalChunks, long maxFileSize)
        {
            //验证参数
            if (chunkNumber == 0 || chunkSize == 0 || totalSize == 0 || identifier.Length == 0 || filename.Length == 0)
            {
                return "invalid params";
            }
            if (chunkNumber > totalChunks)
            {
                return "invalid params :chunkNumber,totalChunks";
            }

            if (chunkNumber < totalChunks && fileSize != chunkSize)
            {

                return "The chunk in the POST request isn't the correct size";
            }

            if (totalChunks == 1 && fileSize != totalSize)
            {
                return "The chunk in the POST request isn't the correct size";
            }

            //大小验证
            if (totalSize > maxFileSize)
            {
                return "The file is too big";
            }

            return null;
        }

        /// <summary>
        /// 合并文件
        /// </summary>
        /// <param name="payload"></param>
        /// <param name="ext"></param>
        /// <param name="chunkFiles"></param>
        /// <returns></returns>
        private async Task<string> MergeChunkFiles(JwtPayload payload, string ext,
            string[] chunkFiles)
        {
            //上传逻辑
            var now = DateTime.Now;
            var yy = now.ToString("yyyy");
            var mm = now.ToString("MM");
            var dd = now.ToString("dd");

            var fileName = Guid.NewGuid().ToString("n") + ext;

            var folder = Path.Combine(_config.PhysicalPath, payload.app, yy, mm, dd);
            if (!Directory.Exists(folder))
            {
                Directory.CreateDirectory(folder);
            }

            var filePath = Path.Combine(folder, fileName);

            using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
            {
                foreach (var chunkFile in chunkFiles.OrderBy(x => int.Parse(x.Substring(x.LastIndexOf(".") + 1))))
                {
                    using (var chunkStream = File.OpenRead(chunkFile))
                    {
                        await chunkStream.CopyToAsync(fileStream);
                    }

                    //是否要删除块
                    File.Delete(chunkFile);
                }
            }

            var fileUrl = _config.RootUrl + "/" + payload.app + "/" + yy + "/" + mm +
                          "/" +
                          dd +
                          "/" + fileName;

            return fileUrl;
        }
    }
}